Skip to content

status: fix FSMonitor history and clean-proof gaps - #74

Open
ttaylorr-oai wants to merge 12 commits into
codex-unstablefrom
tb/codex/fsmonitor-hardlink-inodes-unstable
Open

status: fix FSMonitor history and clean-proof gaps#74
ttaylorr-oai wants to merge 12 commits into
codex-unstablefrom
tb/codex/fsmonitor-hardlink-inodes-unstable

Conversation

@ttaylorr-oai

@ttaylorr-oai ttaylorr-oai commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • retry a late Darwin synchronization cookie only after the existing one-second timeout, with the existing bounded fallback preserved
  • retain a deduplicated, sequence-aware overflow of old FSMonitor paths so private indexes cannot prune or replay history needed by the canonical index
  • preserve authenticated FSMonitor state across clean non-fast-forward merges while conflicts and alternate indexes continue to fail closed
  • publish a clean proof after a writable exact git status repairs the canonical index, without extending the path to optional-lock-disabled commands, option-bearing status calls, or linked worktrees
  • preserve authenticated status and paired untracked-cache proofs across configured fast-forward pulls while retaining the conservative fallback for changed attribute sources

Controller scope

This fast-forwards the existing tb/codex/fsmonitor-hardlink-inodes-unstable topic by seven commits after 88fab89c5e. This pull request is review-only: approve it for controller admission, but do not merge it into codex-unstable.

Performance

  • successful cookie queries keep the existing fast path; the extra flush and wait happen only after the original timeout
  • ordinary batch walks are unchanged; sequence filtering and pointer-identity deduplication are confined to overflow compaction
  • merge and repaired-index handoffs remain restricted to clean canonical-index operations
  • configured pulls ignore command-scoped transport settings in the status fingerprint; large present directory events authenticate each distinct attribute source once, and new tracked directories selectively invalidate the retained untracked cache
  • an exact 5,001-entry configured-pull control with 100 new nested files refreshed only those 100 entries; changing .gitattributes retained the full conservative fallback
  • direct daemon-response A/B: 12 paired rounds of 50 requests over 5,251 paths; paired geometric change -0.88%, paired median change +0.15%, and no compaction in either arm
  • seven ordinary git status scenarios over 12,000 tracked paths reported zero compactions; wall-clock data was nonstationary and is retained as inconclusive

Validation

At source commit f5f8eaf9b2:

  • make -j8
  • ./t7519-status-fsmonitor.sh (107/107)
  • ./t7527-builtin-fsmonitor.sh (178/178)
  • ./t7530-status-clean-sidecar.sh (59/59)
  • ./t/unit-tests/bin/unit-tests -r clean_status_config (14/14)
  • make -C t test-lint
  • git diff --check
  • exact configured-pull controls with unchanged and changed attribute sources

The first six commits remain range-diff identical to the validated current-lane replay at 6314bd17c6; the seventh is the configured-pull release-blocker fix described above.

@ttaylorr-oai
ttaylorr-oai marked this pull request as draft August 26, 2026 18:53
@ttaylorr-oai
ttaylorr-oai removed the request for review from dreynaud-oai August 26, 2026 18:53
@ttaylorr-oai
ttaylorr-oai marked this pull request as ready for review August 26, 2026 19:17
with_lock__wait_for_cookie() gives a filesystem provider one second to
report a synchronization cookie. A healthy FSEvents stream can miss
that deadline while macOS is under load. The daemon then returns a
trivial response, and status scans the entire index even though event
delivery is still making progress.

4b1c56a (fsmonitor: flush pending FSEvents before cookie wait,
2026-07-21) requested an asynchronous flush on every Darwin query but
kept the same one-second deadline. f439708 (Revert "fsmonitor: flush
pending FSEvents before cookie wait", 2026-08-17) reverted it after a
matched 48-query test still saw 12 timeouts in each arm. Avoid restoring
that unqualified hot-path request.

When the initial Darwin wait expires, request an asynchronous FSEvents
flush and wait one more bounded interval. Successful queries retain the
original wait and do not issue a flush or extend their deadline. The
asynchronous call cannot block on the callback while the client holds
main_lock. If the provider stays silent, retain the existing
trivial-response fallback after the retry.

Add a test-only callback delay to exercise both outcomes: a 1.2-second
delay is recovered, while a 2.5-second delay still reaches the bounded
fallback.
The daemon currently assumes that each client which advances an
FSMonitor token also updates the repository's canonical index. That
does not hold for commands using GIT_INDEX_FILE. A private index can
advance the daemon past the canonical index's token and cause the
canonical index's next query to receive a global invalidation.

Keep a deduplicated overflow batch instead of discarding old paths.
Clients at the overflow sequence still get an exact delta. Older
clients get a conservative union of paths, which may overreport but
cannot miss a change.

All paths are interned. Keep a pointer-identity hash set with the
overflow batch so later compactions hash only newly retired paths,
rather than rebuilding a set over the daemon's lifetime history.

Add a regression which advances a private index repeatedly, verifies
that compaction remains deduplicated, and then checks that a read-only
canonical status reports both changed files without a trivial response.
Retired batches are collapsed into a path-only overflow set.  That keeps
old indexes complete, but it loses the sequence in which each path was
last observed.  A client that consumed an inode event can therefore see
it again after another index compacts the batch list, causing repeated
hard-link scans.  Unpinned batches have a zero pinned time and are also
eligible for compaction immediately despite the default grace period.

Do not use unpinned batches as truncation boundaries.  Record the newest
original batch sequence for every overflow path, and filter overflow
responses against the client's requested sequence.  The normal batch
walk remains unchanged; sequence lookups are confined to overflow
responses.

Cover both the default retention grace and the cross-index hard-link
case.  The latter persists a nonzero checkpoint, compacts through a
private index, and verifies repeated canonical reads do not rescan or
fall back to global invalidation.
The delayed-cookie tests send the v1 timestamp token "0" and only check
that the response is nonempty.  Both recovery and fallback can satisfy
that assertion with the same trivial response, so the tests do not
distinguish a rescued cookie from a token-generation reset.

Send a deterministic valid v2 token instead.  Verify that the 1200ms
case preserves its token generation without a global invalidation, while
the 2500ms case changes generation and sends the fallback invalidation.
215845a (fsmonitor: preserve authenticated proofs across ordinary
commands, 2026-08-15) enabled the clean-status history handoff for
merges, but excluded invocations where fast_forward was FF_NO.
Requested merge topology does not determine whether the resulting index
is semantically safe.  A clean non-fast-forward merge can carry the same
authenticated FSUC/FSCF state as a fast-forward merge.

As a result, --no-ff, --no-ff --no-commit, and merge.ff=false all
dropped FSUC and reduced the FSCF flags from 15 to 9 after a clean
merge.  Each subsequent read-only status invalidated the external
history and rescanned the semantic manifest.

Enable the handoff for every merge using the canonical index.  Conflict
handling still invalidates unsafe proofs, and explicit alternate indexes
remain excluded.  Cover all three non-fast-forward forms, repeated
read-only status calls, conflicts, and alternate indexes.
An exact clean status can repair a stale FSMonitor checkpoint or cached
stat data while it scans.  The repair requires an index write, so the
existing issue path leaves no clean sidecar behind.  Read-only callers
then repeat the full scan until a second writable exact status publishes
the proof.

After the repair is written and resumable history is durable, install a
sidecar bound to the rewritten index.  Keep optional-lock-disabled
commands read-only, preserve the literal exact-command restriction, and
do not extend sidecar support to linked worktrees.

Cover repeated read-only scans after a legacy daemon replacement, the
single writable index repair in main and linked worktrees, and the next
read-only sidecar hit in the main worktree.  Keep option-bearing status
commands ineligible for proof publication.
A configured pull can discard each layer of authenticated status history
even when worktree inputs remain unchanged. Command-scoped protocol and
HTTP settings change the config digest, directory events with more than
64 tracked descendants reject the semantic proof, and a fast-forward
which adds an indexed directory drops the paired untracked cache. The
next status can consequently preload and refresh the full index.

Treat command-scoped protocol and HTTP settings as transport-only. For a
large directory event, authenticate each distinct attribute source once
instead of rejecting the cone outright. When a checkout adds tracked
paths, retain the paired untracked cache and replay those additions
through its existing invalidation path.

Cover configured pulls in main and linked worktrees, large directory
events, nested attribute-source changes, and branch switches which add
tracked directories. The conservative full-scan fallback remains in
place when an attribute source changes.
Configured pulls preserve FSMonitor clean proofs when checkout can
authenticate every index change.  Tracked policy files were an
exception: adding or replacing .gitattributes or .gitignore made the
generic semantic transfer reject the whole proof.  Later read-only
status commands then had to rescan the worktree and could not restore
the paired untracked proof.

Let checkout retain history across regular policy-file changes that it
writes itself.  Attribute changes refresh the worktree manifest before
the provider boundary is rebound, and fail closed if that refresh cannot
authenticate the new sources.  Keep the existing untracked-cache
invalidation for ignore changes, and transfer that cache only while the
full tracked proof remains current.

Exercise configured fast-forward pulls in main and linked worktrees.  A
required-filter control also verifies that changed attributes invalidate
the affected tracked entry instead of certifying it.
@ttaylorr-oai
ttaylorr-oai force-pushed the tb/codex/fsmonitor-hardlink-inodes-unstable branch from f5f8eaf to 0a572e5 Compare August 27, 2026 04:33
A clean status proof can survive a pull only when its configuration,
tracked-file state, FSMonitor token, and paired untracked cache still
describe the resulting worktree. Command-scoped push transport settings
were included in the configuration fingerprint. Checkout could also
discard the untracked proof for policy-file changes or leave events from
its own worktree writes outside the proof.

The next diff, write-tree, or status then repeated tracked and untracked
work. With optional locks disabled, status could not publish the repair,
so each invocation paid the same cost.

Treat push.negotiate and remote.*.pushurl like other command-scoped
transport settings. Preserve the paired untracked cache across checkout,
invalidate only affected policy scopes, and authenticate distinct
attribute-source directories before transferring semantic history.

For checkout, reset, merge, and sequencer worktree updates, write a
provisional index under the existing lock, consume the daemon events
caused by the update, and certify the result against that locked index
before the final write. This also covers stash cleanup through its hard
reset. Alternate indexes, split or sparse indexes, unsafe filter or
manifest state, and incomplete stat data still fall back.

Cover configured pulls, root and nested policy changes, main and linked
worktrees, rebase, reset, stash, checkout, and repeated read-only
status.
The Linux listener queued every inotify event as a file pathname.
Directory events therefore lacked the trailing slash used by semantic
invalidation. After an owned worktree update retained a clean proof, a
following read-only status could not close those events against it. The
command scanned all tracked entries. With core.preloadIndexBulk enabled,
this work appears as statx calls instead of lstat counters.

Format Linux worktree events through
fsmonitor_format_worktree_paths() and use IN_ISDIR to preserve their
directory identity. Advertise directory metadata support and mark Linux
tokens so clients replace daemons using the old event format.

Concurrent clients can see an expected connection reset while one client
replaces a stale daemon. Silence that diagnostic only for gentle IPC
reads, which already reconnect, without changing ordinary IPC error
handling.

Cover both bulk preload modes plus single and concurrent daemon
replacement.
Bulk index preload can defer content and conversion checks to the diff
that normally follows refresh_index(). repair_fsmonitor_proof() only
refreshes the index before deciding whether to persist a clean proof; it
does not run that diff. With core.preloadIndexBulk enabled, a pull or
rebase that changes .gitattributes or .gitignore can therefore leave
tracked entries dirty after the writer reports a successful repair.
Repeated read-only status calls cannot persist the missing repairs.

Do not request deferred bulk results in the writer-repair path. This
keeps the ordinary status and diff bulk path unchanged while forcing the
exceptional repair to finish its tracked checks before certifying and
writing the proof.

Enable bulk preload in the existing fast-forward, policy-file, and
sequencer writer tests. They verify targeted refreshes and two
subsequent read-only status calls without scans or index writes.
A writable Git command can leave a complete FSMonitor proof in a
repairable state when it changes policy files, adds or removes an
intent-to-add entry, or delegates the final index write to a child
process. The existing repair path assumed that the manifest and
untracked cache remained closed. The sequencer also kept its stale
in-memory index after git commit rewrote the canonical index. Stash
operations and completed rebases could therefore drop FSUC or overwrite
the child's newer token. Read-only status could not persist the repair
and repeated tracked or directory work.

Let index-only writers refresh changed manifests and rebuild the paired
untracked cache against the provisional locked index. Preserve unrelated
history for safe intent-to-add changes and unmerged non-attribute paths,
then reload the canonical index after child writers before repairing it.
Active filters and unresolved structural indexes still fall back.

Linux can report an event for a watched directory without a child name.
Keep the watched directory in that case, encode its token capabilities
in the order understood by Linux clients, and serialize incompatible
daemon replacement on Linux as on macOS.

Cover stash creation and application, policy-file updates, ordinary and
--rebase-merges conflict completion, cherry-pick's deliberately weaker
tracked-only proof, nameless inotify events, and primary and linked
worktrees. Repeated optional-lock-free status calls must not rewrite the
index or rescan tracked entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant